import os
import wmi
import time
import tkinter as tk
import tkinter.messagebox as tkm
from pathlib import Path

import cv2
import torch
import logging
import datetime
import configparser
import torch.backends.cudnn as cudnn
from tkinterdnd2 import *
import tkinter.filedialog as tkf
from models.experimental import attempt_load
from utils.datasets import LoadStreams, LoadImages
from utils.general import non_max_suppression, scale_coords, xyxy2xywh, set_logging, increment_path, save_one_box, \
    MyThread, imgConvert, LoggerBox
from utils.plots import colors, plot_one_box
from utils.torch_utils import time_synchronized

content = configparser.ConfigParser()
content.read("config.ini", encoding='UTF-8')

# 输入图像为640x480
file = content.get("detect", "file")
# 初始化摄像头数等于0
camera_num = 0
# 选择的摄像头编号
camera_No = "0"
# 初始化半精度
half = None
weight = content.get("detect", "weight")
conf_thres = eval(content.get("detect", "conf_thres"))
iou_thres = eval(content.get("detect", "iou_thres"))
bg = content.get("detect", "bg_image")
# 计算设备【cpu|gpu】
computer_device = content.get("detect", "computer_device")
# 检测类型【file|camera】
detect_type = content.get("detect", "detect_type")
# 窗口左上角图标
mask_icon = content.get("detect", "mask_icon")
# 画布
canvas = None
# 临时变量引用canvas，防止不生效
file_temp = None
# 输出文件夹
save_dir = None
# 模型
model = None
# 类别数组
names = None
# Set Dataloader
vid_path, vid_writer, dataset = None, None, None
# 主窗口
root = None
# 停止检测
stop_detect = False
# 日志
log_box, log_path = None, content.get("detect", "log_path")

# 可检测文件类型
image_formats = ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'dng', 'webp', 'mpo']  # 可检测图片格式
video_formats = ['mov', 'avi', 'mp4', 'mpg', 'mpeg', 'm4v', 'wmv', 'mkv']  # 可检测视频格式


################################################################## 获取设备信息 ##############################################################
def get_camrea(detail=True):
    info = wmi.WMI()
    wql = "Select * From Win32_USBControllerDevice"

    device_num = 0
    if detail:
        print_save_log("▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼设备信息▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼")
    for item in info.query(wql):
        a = item.Dependent.PNPClass
        b = item.Dependent.Name.upper()
        if (a.upper() == 'MEDIA' or a.upper() == 'CAMERA') and 'AUDIO' not in b:
            device_num += 1
            if detail:
                print_save_log(item.Dependent)
    if detail:
        print_save_log(f"该设备共有{device_num}个摄像头")
    return device_num
################################################################# 获取设备信息 END ############################################################



################################################################# 控件状态更改实现 #############################################################
def change_widget_state(state=True):
    # True为启用控件，用于检测结束
    # False为禁用控件，用于检测开始

    # 开始检测，禁用重置系统控件
    load_reset_system_widget(state=state)
    # 启用停止检测控件（特例）
    load_stop_detect_widget(state=not state)
    # 禁用选择摄像头控件
    load_camera_ra_widget(state=state)
    # 禁用选择权重控件
    load_select_weight_widget(state=state)
    # 禁用自动输出控件
    load_auto_select_dir_widget(state=state)
    # 禁用检测类型控件
    load_type_widget(state=state)
############################################################### 控件状态更改实现 END ###########################################################



################################################################# 检测实现 #############################################################
@torch.no_grad()
def start_detect():

    global dataset, vid_path, model, vid_writer, canvas, root, stop_detect, \
        computer_device, detect_type, file, camera_No, weight, file_temp, save_dir, half


    if model == None:
        print_save_log("检测终止")
        print_save_log("模型尚未初始化，请先加载模型")
        return
    # 开始检测，禁用部分控件
    change_widget_state(state=False)

    print_save_log(f"计算设备：{computer_device}")
    print_save_log(f"检测类型：{detect_type}")
    print_save_log(f"加载权重：{weight}")
    print_save_log(f"网络结构\n{model}")
    print_save_log(f"输出路径：{save_dir}")

    # 自动时创建增量路径及txt文件夹
    (save_dir / 'labels').mkdir(parents=True, exist_ok=True)  # make dir

    # 初始化改变停止标志防止无法检测
    stop_detect = False

    # 摄像头和图片检测分开处理加载数据源
    if detect_type == "camera":
        if camera_num == 0:
            # 停止检测，启用部分控件
            change_widget_state(state=True)
            print_save_log("无摄像头，检测终止！")
            return
        print_save_log("即将进行摄像头检测！")
        print_save_log(f"检测对象：{camera_No}号摄像头")
        # 读取视频流
        cudnn.benchmark = True  # set True to speed up constant image size inference
        dataset = LoadStreams(camera_No)

    if detect_type == "file":
        print_save_log("即将进行文件检测")
        print_save_log(f"检测对象：{file}")
        # 读取图片/视频
        dataset = LoadImages(file)

    # 重新指向计算设备为cpu或0（gpu）
    device = computer_device
    if computer_device == "gpu":
        device = torch.device('cuda:0')
        # half precision only supported on CUDA
        half = True
        # to FP16
        model.half()

    t0 = time.time()
    count = 0
    count_fps = 0
    # 从上面的LoadImages()就可以看到每次只输出单张图片,这边img: (C,H,W)
    # 如果每次要输出好几张图片传入模型,那么就需要修改LoadImages()了,那么这里img才会变成(N,C,H,W)
    for path, img, im0s, vid_cap in dataset:
        # 处理的对象是视频
        if hasattr(dataset, 'frame'):
            print_save_log(f'正在处理视频 {file} (第{dataset.frame}帧/共{dataset.frames}帧)')
        # 代表检测是图片
        elif detect_type != "camera":
            print_save_log(f'正在处理图片 {file}')
        # 读取图片,归一化等操作
        img = torch.from_numpy(img).to(device)
        img = img.half() if half else img.float()  # uint8 to fp16/32
        img /= 255.0
        # 如果是单张图片，那么(C,H,W)升维成(1,C,H,W)
        if img.ndimension() == 3:
            img = img.unsqueeze(0)
        # Inference
        t1 = time_synchronized()
        ''' 
            获得预测的结果pred的结果为例如为：torch.Size([1, 18900, 85])， img为(1,3,640,480),
            模型的下采样倍率为[16,32],(640/32*480/32+640/16*480/16)*3=4500,也就是特征图上格子数.
        '''
        pred = model(img, augment=False)[0]

        # 进行非极大值抑制
        pred = non_max_suppression(pred, conf_thres=conf_thres, iou_thres=iou_thres)
        t2 = time_synchronized()

        count_fps += (int((1 / (t2 - t1)) * 100)) / 100
        count += 1
        # 计算fps
        fps = (int((1 / (t2 - t1))*100))/100
        # 检测过程
        for i, det in enumerate(pred):  # detections per image
            if detect_type == "camera":  # batch_size >= 1
                p, s, im0, frame = path[i], f'{i}: ', im0s[i].copy(), dataset.count
            else:
                p, s, im0, frame = path, '', im0s.copy(), getattr(dataset, 'frame', 0)
            # 定义图片,txt等存储的地址
            p = Path(p)  # to Path
            save_path = str(save_dir / p.name)  # img.jpg
            txt_path = str(save_dir / 'labels' / p.stem) + (
                '' if dataset.mode == 'image' else f'_{frame}')  # img.txt
            s += '图像大小「%g, %g」' % img.shape[2:]  # print string
            # 因为pred结果的box是在加边(padding)后的图片上的坐标,所以要还原到原图的坐标
            gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]  # normalization gain whwh
            imc = im0.copy()  # for opt.save_crop
            # 无目标时显示fps
            plot_one_box(None, im0, label=None, color=(255, 0, 255), line_thickness=2, fps=fps)
            # 包含目标
            if len(det):
                # 加边后图的坐标转为原图坐标
                # Rescale boxes from img_size to im0 size
                det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round()
                # Print results
                # 统计目标数
                object_num = 0
                s += "» 类别『"
                for c in det[:, -1].unique():
                    n = (det[:, -1] == c).sum()  # detections per class
                    # 统计类别
                    s += f"{n}个{names[int(c)]}｜"  # add to string
                    object_num += 1
                # 去掉尾部｜
                s = s[0:-1]
                s += f"』〖共{object_num}个目标〗"
                # Write results
                for *xyxy, conf, cls in reversed(det):
                    # 保存检测到的标签txt
                    xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()  # normalized xywh
                    # line = (cls, *xywh, conf) if opt.save_conf else (cls, *xywh)  # label format
                    line = (cls, *xywh, conf)  # label format
                    with open(txt_path + '.txt', 'a') as f:
                        f.write(('%g ' * len(line)).rstrip() % line + '\n')
                    # 给推理的图片加box
                    # 保存检测图
                    c = int(cls)  # integer class
                    # 图片上绘制的文字
                    label = f'{names[c]} {conf:.2f}'
                    # 将框画在图中
                    plot_one_box(xyxy, im0, label=label, color=colors(c, True), line_thickness=3,
                                 fps=fps)
                    # 将检测到的物体裁剪保存
                    save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True)

            # 未检测到目标
            else:
                s += "未检测到目标"

            # 输出目标分类信息、目标总数 单帧用时、fps信息
            print_save_log(f'{s} » 用时{t2 - t1:.3f}s » {fps}fps » 平均{(int((count_fps/count)*100))/100}fps ')
            # 绘图至canvas
            tkimg = imgConvert.convert(im0)
            canvas.create_image(0, 0, anchor='nw', image=tkimg, tag='im0')
            file_temp = tkimg
            root.update()
            root.after(1)

            # 文件类型且格式为图片的单张图片检测显示
            if detect_type == "file" and file.split(".")[-1] in image_formats:
                tkimg = imgConvert.convert(im0, resize=True)
                file_temp = tkimg
                canvas.create_image(0, 0, anchor='nw', image=tkimg, tag='im0')
                root.update()
                # 检测结束，启用部分控件
                change_widget_state(state=True)


            # 停止检测
            if stop_detect:
                # 以tag为标记删除背景
                reset_canvas()
                print_save_log("停止检测")
                # 停止检测标志更新
                stop_detect = False

                # 释放摄像头
                dataset.cap.release()
                cv2.destroyAllWindows()

                root.update()
                # 检测结束，启用部分控件
                change_widget_state(state=True)
                return

            # 保存结果，图片就保存图片，视频就保存视频片段
            if dataset.mode == 'image':
                cv2.imwrite(save_path, im0)
            if dataset.mode == 'video' or dataset.mode == 'stream':  # 'video' or 'stream'
                if vid_path != save_path:  # new video
                    vid_path = save_path
                    if isinstance(vid_writer, cv2.VideoWriter):
                        vid_writer.release()  # release previous video writer
                    if vid_cap:  # video
                        fps = vid_cap.get(cv2.CAP_PROP_FPS)
                        w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
                        h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
                    else:  # stream
                        fps, w, h = 30, im0.shape[1], im0.shape[0]
                        save_path += '.mp4'
                    vid_writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
                vid_writer.write(im0)
    # 保存图和txt
    s = f"\n{len(list(save_dir.glob('labels/*.txt')))} 标签文件保存到 {save_dir / 'labels'}"
    print_save_log(f"检测结果保存到 {save_dir}{s}")
    print_save_log(f'检测完成，用时{time.time() - t0:.3f}s')

    # 检测结束，启用部分控件
    change_widget_state(state=True)
    # 释放摄像头
    if detect_type=="camera":
        dataset.cap.release()
        cv2.destroyAllWindows()
############################################################### 检测实现 END ############################################################



############################################################### 初始化窗口参数 ###########################################################
def init_window():
    global root, camera_num
    root = TkinterDnD.Tk()
    root.title('口罩佩戴检测')  # 窗口标题
    root.geometry('1300x800+300+100')  # 窗口大小
    root.resizable(0, 0)  # 禁止缩放
    root.config(bg='#F0F0F0')  # 窗口背景颜色
    root.iconbitmap(mask_icon)
    camera_num = get_camrea(detail=False)

init_window()
############################################################## 初始化窗口参数 END #########################################################




################################################################# 初始化画布 #############################################################
def load_canvas():
    global canvas, file_temp
    canvas = tk.Canvas(root, bg='black', width=640, height=480)
    canvas.place(x=150, y=60)

load_canvas()
############################################################### 初始化画布 END ###########################################################




################################################################## 重置画布 ##############################################################
def reset_canvas():
    global canvas, file_temp, bg
    bg_ = cv2.imread(bg)
    bg_ = imgConvert.convert(bg_, True)
    file_temp = bg_
    canvas.create_image(0, 0, anchor='nw', image=bg_, tag='bg')
################################################################ 重置画布 END ###########################################################




############################################################### 初始化日志框 ###########################################################
def load_log_box():
    global log_box, root
    streamHandlerBox = LoggerBox(root, width=125, height=12)
    streamHandlerBox.place(x=80, y=560)
    log_box = logging.getLogger('log')
    log_box.setLevel(logging.INFO)
    log_box.addHandler(logging.StreamHandler(streamHandlerBox))

load_log_box()
############################################################## 初始化日志框 END ##########################################################




################################################################## 获取时间 ##############################################################
def get_time(time_format="day"):
    now_time = str(datetime.datetime.now())
    sec_time = now_time.split(".")[0]
    day_time = now_time.split(" ")[0]
    if time_format == "day":
        return day_time
    elif time_format == "sec":
        return sec_time
################################################################ 获取时间 END ############################################################




############################################################# 输出同时打印日志信息 #########################################################
def print_save_log(text, startTime=True):
    global log_box, log_path
    # 无日志文件夹，先创建
    if not os.path.exists(log_path):
        os.mkdir(log_path)
    if not isinstance(text, str):
        text = str(text)

    file_name = log_path + get_time(time_format="day") + ".log"
    # 开始加时间
    if startTime:
        text = get_time(time_format="sec") + ": " + text
    # 不换行直接拼接文本内容
    log_box.info(text)
    with open(file_name, 'a+', encoding="utf-8") as f:
        f.write(text)
        f.write('\n')
########################################################## 输出同时打印日志信息 END #######################################################




############################################################## 加载选择摄像头控件 ############################################################
def load_camera_ra_widget(state=True):
    global camera_num, camera_No
    camera_var = tk.IntVar()
    # 设备标签
    device_label = tk.Label(root, width=13, height=1, text='empty', font=("微软雅黑", 9, "bold"))
    device_label.place(x=860, y=82)

    camera_var.set(0)
    device_label.config(text='摄像头「' + str(camera_var.get()) + '」')

    # 选择摄像头触发函数
    def camera_trigger():
        global computer_device, camera_No
        device_label.config(text='摄像头「' + str(camera_var.get()) + '」')
        camera_No = str(camera_var.get())
        print_save_log(f"选择了{camera_No}号摄像头")

    for i in range(camera_num):
        # camera Radiobutton
        locals()["camera"+str(i+1)] = tk.Radiobutton(root, text="# "+str(i), variable=camera_var, value=i, command=camera_trigger, font=("微软雅黑", 9, "bold"), cursor="hand2")
        locals()["camera"+str(i+1)].place(x=1030+i*90, y=80)

    if camera_num == 0:
        no_device_label = tk.Label(root, width=13, height=1, text='无可用摄像头', font=("微软雅黑", 9, "bold"))
        no_device_label.place(x=1000, y=82)

    if not state:
        for i in range(camera_num):
            locals()["camera"+str(i+1)].config(state='disable')
        print_save_log("选择摄像头控件已禁用！")
    else:
        print_save_log("选择摄像头控件加载成功！")

load_camera_ra_widget() if camera_num  else load_camera_ra_widget(state=False)
############################################################# 加载选择摄像头控件 END #######################################################




############################################################## 加载设备选择控件 ############################################################
def load_device_select_widget(state=True):
    device_var = tk.IntVar()
    # 设备标签
    device_label = tk.Label(root, width=13, height=1, text='empty', font=("微软雅黑", 9, "bold"))
    device_label.place(x=860, y=122)
    device_dict = {0: "CPU", 1: "GPU"}
    if computer_device == "cpu":
        device_var.set(0)
    else:
        device_var.set(1)
    device_label.config(text='计算设备「' + device_dict[device_var.get()] + '」')

    # 设备选择触发函数
    def device_trigger():
        global computer_device
        device_label.config(text='计算设备「' + device_dict[device_var.get()] + '」')
        if device_var.get() == 0:
            computer_device = "cpu"
        if device_var.get() == 1:
            computer_device = "gpu"
        print_save_log(f"选择了设备：{computer_device}")

    # CPU Radiobutton
    cpu_ra = tk.Radiobutton(root, text='CPU', variable=device_var, value=0, command=device_trigger, font=("微软雅黑", 9, "bold"), cursor="hand2")
    cpu_ra.place(x=1030, y=120)
    # GPU Radiobutton
    gpu_ra = tk.Radiobutton(root, text='GPU', variable=device_var, value=1, command=device_trigger, font=("微软雅黑", 9, "bold"), cursor="hand2")
    gpu_ra.place(x=1120, y=120)

    if not torch.cuda.is_available() or not state :
        cpu_ra.config(state='disable')
        gpu_ra.config(state='disable')
        print_save_log("设备选择控件已禁用！")
    else:
        print_save_log("设备选择控件加载成功！")

load_device_select_widget(state=True)
############################################################ 加载设备选择控件 END ###########################################################




############################################################# 加载检测类型控件 ############################################################
def load_type_widget(state=True):
    global detect_type, camera_num
    type_var = tk.IntVar()
    # 设备标签
    type_Label = tk.Label(root, width=15, height=1, text='empty', font=("微软雅黑", 9, "bold"))
    type_Label.place(x=860, y=162)

    type_dict = {0: "文件", 1: "摄像头"}
    # 检测到没有摄像头
    if detect_type == "file":
        type_var.set(0)
    else:
        type_var.set(1)
    type_Label.config(text='检测类型「' + type_dict[type_var.get()] + '」')

    # 设备选择触发函数
    def type_trigger():
        global detect_type, select_file_entry, select_file_btn
        type_Label.config(text='检测类型「' + type_dict[type_var.get()] + '」')
        if type_var.get() == 0:
            detect_type = "file"
            print_save_log("使用文件检测！")
            # 文件检测启用文件选择控件
            load_select_file_widget(state=True)
            # 文件检测禁用摄像头选择控件
            load_camera_ra_widget(state=False)
        if type_var.get() == 1:
            detect_type = "camera"
            print_save_log("使用摄像头检测！")
            # 摄像头检测禁用文件选择控件
            load_select_file_widget(state=False)
            # 摄像头检测启用摄像头选择控件
            load_camera_ra_widget(state=True)


    image_ra = tk.Radiobutton(root, text='文件', variable=type_var, value=0, command=type_trigger, font=("微软雅黑", 9, "bold"), cursor="hand2")
    image_ra.place(x=1030, y=160)
    # 视频 Radiobutton
    video_ra = tk.Radiobutton(root, text='摄像头', variable=type_var, value=1, command=type_trigger, font=("微软雅黑", 9, "bold"), cursor="hand2")
    video_ra.place(x=1120, y=160)

    if not state:
        image_ra.config(state='disable')
        video_ra.config(state='disable')
        print_save_log("检测类型控件已禁用！")
    else:
        print_save_log("检测类型控件加载成功！")

load_type_widget(state=True)
############################################################ 加载检测类型控件 END ##########################################################




############################################################### conf阈值控件 ##############################################################
def conf_thres_widget():
    conf_thres_label = tk.Label(root, text='conf-thres「' + str(conf_thres) + '」', font=("微软雅黑", 9, "bold"))
    conf_thres_label.place(x=840, y=230)

    def print_conf_thres_selection(value):
        global conf_thres
        conf_thres = float(value)
        conf_thres_label.config(text='conf-thres「' + str(conf_thres) + '」')

    # iou-thres Scale
    conf_thres_scale = tk.Scale(root, from_=0, to=1, orient=tk.HORIZONTAL, length=250, showvalue=0, tickinterval=0.2,
                                resolution=0.01,
                                command=print_conf_thres_selection, cursor="hand2")
    conf_thres_scale.place(x=1000, y=230)
    conf_thres_scale.set(conf_thres)
    print_save_log("conf阈值控件加载成功！")

conf_thres_widget()
############################################################# conf阈值控件 END ############################################################




############################################################### iou阈值控件 ##############################################################
def iou_thres_widget():
    iou_thres_label = tk.Label(root, text='iou-thres「' + str(iou_thres) + '」', font=("微软雅黑", 9, "bold"))
    iou_thres_label.place(x=840, y=290)

    def print_iou_thres_selection(value):
        global iou_thres
        iou_thres = float(value)
        iou_thres_label.config(text='iou-thres「' + str(iou_thres) + '」')

    # iou-thres Scale
    iou_thres_scale = tk.Scale(root, from_=0, to=1, orient=tk.HORIZONTAL, length=250, showvalue=0, tickinterval=0.2,
                               resolution=0.01,
                               command=print_iou_thres_selection, cursor="hand2")
    iou_thres_scale.place(x=1000, y=290)
    iou_thres_scale.set(iou_thres)
    print_save_log("iou阈值控件加载成功！")

iou_thres_widget()
############################################################ iou阈值控件 END ###########################################################




########################################################## 加载选择图片/视频控件 #########################################################
file_path = tk.StringVar()

def load_select_file_widget(state=True):

    def select_file():
        global file_path, file, canvas, root, file_temp

        path = tkf.askopenfilename(
            filetypes=[("Image File", image_formats), ("Video File", video_formats),
                       ("All File", image_formats + video_formats)])

        if path:
            file_path.set(path)
            file = path
            if path.split(".")[-1] in image_formats:
                # 读取图片并显示到窗口
                tkimg = cv2.imread(path)
                tkimg = imgConvert.convert(tkimg, resize=True)
                file_temp = tkimg
                canvas.create_image(0, 0, anchor='nw', image=tkimg, tag='im0')
                root.update()
            # 输出日志
            print_save_log(f"已选择文件 {path}")
            # 更新文件
            content.set("detect", "file", path)
            content.write(open("config.ini", "w+", encoding="UTF-8"))
        else:
            print_save_log("用户取消选择文件！")

    select_file_entry = tk.Entry(root,
                                 textvariable=file_path,
                                 width=35)
    select_file_entry.place(x=850, y=360)

    file_path.set(file)

    select_file_btn = tk.Button(root, text="选择文件", command=select_file, relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    select_file_btn.place(x=1175, y=358)

    if not state:
        select_file_entry.config(state='disable')
        select_file_btn.config(state='disable')
        print_save_log("选择图片/视频控件已禁用！")
    else:
        print_save_log("选择图片/视频控件加载成功！")

load_select_file_widget(state=False)
######################################################### 加载选择图片/视频控件 END ########################################################




############################################################ 加载选择权重控件 ###########################################################
weight_path = tk.StringVar()
def load_select_weight_widget(state=True):
    def select_weight():
        global weight_path, weight, content, model
        weight_format = ['pt', 'pth']  # 可加载权重格式
        path = tkf.askopenfilename(
            filetypes=[("weight file", weight_format)])
        if path == weight:
            return
        # 模型加载完成后，权重加载不生效
        if path:
            weight_path.set(path)
            weight = path
            # 输出日志
            print_save_log(f"选择权重文件 {weight}")
            # 固定权重保存到配置文件中
            content.set("detect", "weight", path)
            content.write(open("config.ini", "w+", encoding="UTF-8"))
            # 根据模型是否加载输出权重更新成功与否
            if model != None:
                # tkm.showinfo(title="提示", message="模型加载后权重更新不成功，下次生效")
                print_save_log("模型加载后权重更新不成功，下次生效")
            else:
                print_save_log("权重更新成功")
                # tkm.showinfo(title="提示", message="权重更新成功")
        # 用户取消更改权重
        else:
            print_save_log("用户取消更改权重")

    select_weight_entry = tk.Entry(root, textvariable=weight_path, width=35)
    select_weight_entry.place(x=850, y=420)

    weight_path.set(weight)

    select_weight_btn = tk.Button(root, text="选择权重", command=select_weight, relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    select_weight_btn.place(x=1175, y=418)

    if not state:
        select_weight_entry.config(state='disable')
        select_weight_btn.config(state='disable')
    print_save_log("选择权重控件加载成功！")

load_select_weight_widget()

############################################################ 加载选择权重控件 END ###########################################################




############################################################ 加载输出文件夹控件 ###########################################################
output_dir_var = tk.StringVar()

# 初始化输出结果文件夹控件
def load_output_dir_widget(state=True):
    global save_dir

    def select_dir():
        global save_dir

        output_dir = tkf.askdirectory()
        if output_dir:
            output_dir = increment_path(Path(output_dir) / "exp", exist_ok=False)
            output_dir_var.set(str(output_dir))
            save_dir = output_dir

    select_output_dir_entry = tk.Entry(root, textvariable=output_dir_var, width=35)
    select_output_dir_entry.place(x=850, y=480)

    if save_dir == None:
        save_dir = increment_path(Path("runs/detect") / "exp", exist_ok=False)
        output_dir_var.set(save_dir)

    select_output_dir_btn = tk.Button(root, text="输出位置", command=select_dir, relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    select_output_dir_btn.place(x=1175, y=478)

    if not state:
        select_output_dir_entry.config(state='disable')
        select_output_dir_btn.config(state='disable')
        print_save_log("选择输出文件夹控件已禁用！")
    else:
        print_save_log("选择输出文件夹控件加载成功！")

load_output_dir_widget(state=False)
############################################################ 加载输出文件夹控件 END ###########################################################




############################################################# 自动选择文件夹控件 ############################################################
auto_select_dir_var = tk.IntVar()


def load_auto_select_dir_widget(state=True):
    def change_auto_select_dir():
        global auto_select_dir_var
        # 选中自动选择文件夹后，锁定无法选择更改文件夹
        if auto_select_dir_var.get() == 1:
            load_output_dir_widget(state=False)
        else:
            load_output_dir_widget(state=True)

    # 自动选择输出文件夹
    auto_select_dir_cbtn = tk.Checkbutton(root, text='自动选择', variable=auto_select_dir_var, onvalue=1, offvalue=0,
                                          command=change_auto_select_dir, font=("微软雅黑", 9, "bold"), cursor="hand2")
    auto_select_dir_cbtn.place(x=845, y=510)
    auto_select_dir_var.set(1)

    if not state:
        auto_select_dir_cbtn.config(state='disable')
        print_save_log("自动选择文件夹控件已禁用！")
    else:
        print_save_log("自动选择文件夹控件加载成功！")

load_auto_select_dir_widget(state=True)
########################################################### 自动选择文件夹控件 END ##########################################################




############################################################### 预加载模型控件 ##############################################################
def load_model_widget():
    def load_model():
        global model, names, computer_device, weight
        device = computer_device
        if computer_device == "gpu":
            device = torch.device('cuda:0')
        # 加载模型
        model = attempt_load(weight, map_location=device)  # load FP32 model
        # 从模型中获取到类别数组
        names = model.names
        # 初始化日志
        set_logging()

        # tkm.showinfo(title="提示", message="模型加载成功")
        print_save_log(f"网络结构\n{model}")
        print_save_log(f"权重文件：{weight}")
        print_save_log(f"计算设备：{computer_device}")
        print_save_log("模型加载成功！")
        if computer_device == "cpu":
            print_save_log(f"注意：加载模型使用了{computer_device}，检测时计算设备禁止更改为gpu")
        if computer_device == "gpu":
            print_save_log(f"注意：加载模型使用了{computer_device}，检测时计算设备禁止更改为cpu")

        # 模型加载完，禁用cpu，gpu选择按钮
        load_device_select_widget(state=False)

    load_model_btn = tk.Button(root, text='加载模型', command=lambda: MyThread(load_model), relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    load_model_btn.place(x=40, y=80)
    print_save_log("加载模型控件加载成功！")

load_model_widget()
############################################################# 预加载模型控件 END #############################################################




############################################################## 加载开始检测控件 #############################################################
def load_camera_widget():
    # 打开摄像头按钮
    open_camera_btn = tk.Button(root, text='开始检测', command=lambda: MyThread(start_detect), relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    open_camera_btn.place(x=40, y=160)
    print_save_log("开始检测控件加载成功！")

load_camera_widget()
############################################################ 加载开始检测控件 END ###########################################################




############################################################# 加载停止检测控件 ############################################################
def load_stop_detect_widget(state=True):
    def stop_detect():
        global stop_detect
        stop_detect = True

    # 打开摄像头按钮
    stop_detect_btn = tk.Button(root, text='停止检测', command=lambda: MyThread(stop_detect), relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    stop_detect_btn.place(x=40, y=240)

    if not state:
        stop_detect_btn.config(state='disable')
        print_save_log("停止检测控件已禁用！")
    else:
        print_save_log("停止检测控件加载成功！")

load_stop_detect_widget(state=False)
############################################################ 加载停止检测控件 END ##########################################################




############################################################# 加载重置系统控件 ############################################################
def load_reset_system_widget(state=True):

    def reset_system():
        global dataset, canvas, model, names
        if dataset != None:
            dataset = None
        if model != None:
            model = None
            # 模型置空，设备选择释放，且
            load_device_select_widget(state=True)

        if names != None:
            names = None

        reset_canvas()

        # tkm.showinfo(title="提示", message="系统重置成功")
        print_save_log("系统重置成功")

    reset_system_btn = tk.Button(root, text='重置系统', command=lambda: MyThread(reset_system), relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    reset_system_btn.place(x=40, y=320)

    if not state:
        reset_system_btn.config(state='disable')
        print_save_log("重置系统控件已禁用！")
    else:
        print_save_log("重置系统控件加载成功！")

load_reset_system_widget(state=True)
############################################################ 加载重置系统控件 END ##########################################################




############################################################## 加载检测结果控件 ############################################################
def load_detect_result_widget():
    def open_result():
        global save_dir

        if save_dir == None:
            print_save_log("暂无运行结果，请检测后再打开")
        else:
            output_dir = str(save_dir)
            # 找不到:说明路径为相对路径 runs\detect\exp4
            if output_dir.find(":") == -1:
                output_dir = os.getcwd() + "\\" + output_dir
            try:
                os.startfile(output_dir)
            except FileNotFoundError:
                print_save_log("暂无结果，请检测后再打开")

    detect_result_btn = tk.Button(root, text='检测结果', command=open_result, relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    detect_result_btn.place(x=40, y=400)
    print_save_log("检测结果控件加载成功！")

load_detect_result_widget()
############################################################ 加载检测结果控件 END ##########################################################




################################################################ 加载获取设备按钮 ############################################################
def load_get_device_widget():

    get_device_btn = tk.Button(root, text='获取设备', command=get_camrea, relief=tk.RIDGE, bd=2, font=("微软雅黑", 9, "bold"), cursor="hand2")
    get_device_btn.place(x=40, y=480)


load_get_device_widget()
############################################################# 加载获取设备按钮 END #########################################################




################################################################# 手动关闭程序 ############################################################
def close():
    if tkm.askokcancel("关闭", "是否关闭程序"):
        print_save_log("程序退出")
        cv2.destroyAllWindows()
        root.destroy()
############################################################### 手动关闭程序 END ##########################################################

print_save_log("控件全部加载成功！")
print_save_log("▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼当前信息▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼")
print_save_log(f"摄像头：{camera_No}号")
print_save_log(f"权重文件：{weight}")
print_save_log(f"计算设备：{computer_device}")
print_save_log(f"检测结果目录：{save_dir}")

root.protocol("WM_DELETE_WINDOW", close)
root.mainloop()
